using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Net.Sockets;
using System.IO;
using System.Windows.Forms;

namespace Christian.Helle.PocketNews.PocketPC.NewsProtocol
{
	public partial class Nntp : /*Component,*/ IDisposable
	{
		private TcpClient tcpServer;

		#region Properties
		/// <summary>
		/// Whether the NNTP Connection is connected
		/// </summary>
		public bool IsConnected;

		/// <summary>
		/// Gets the name of the server
		/// </summary>
		public string ServerName;

		/// <summary>
		/// Gets / Sets the connection timeout for the server
		/// </summary>
		public int Timeout;

		/// <summary>
		/// Gets a bool whether posting is allowed on this server
		/// </summary>
		public bool IsPostingAllowed;

		/// <summary>
		/// Currently selected newsgroup on the news server
		/// </summary>
		public Newsgroup CurrentNewsgroup;

		/// <summary>
		/// Indicating whether events are fired
		/// </summary>
		public bool EnableEvents;

		#endregion

		/// <summary>
		/// Attempt to connect to the News Server whose address is specified 
		/// at the object's ServerName property.
		/// </summary>
		/// <returns>Boolean value indicating whether the connection is successful</returns>
		public bool Connect()
		{
			if (this.ServerName == "") {
				if (this.EnableEvents && this.OnConnectFailure != null)
					this.OnConnectFailure(this, "Server Name Missing");
				return false;
			}

			return this.Connect(this.ServerName, 119);
		}

		/// <summary>
		/// Attempt to connect to the news Server
		/// </summary>
		/// <param name="address">Address of the Server</param>
		/// <returns>Boolean value indicating whether the connection is successful</returns>
		public bool Connect(string address)
		{
			return this.Connect(address, 119);
		}

		/// <summary>
		/// Attempt to connect to the news server
		/// </summary>
		/// <param name="address">Address of the Server</param>
		/// <param name="port">Port to use for the connection</param>
		/// <exception cref="System.Exception">Thrown when error occurs during the
		/// initialization of the tcpclient object</exception>
		/// <returns>True or false denoting whether the connection is successful</returns>
		public bool Connect(string address, int port)
		{
			// Check whether we are connected to other servers.
			// If so, disconnect from the old server first
			if (this.IsConnected) {
				if (this.EnableEvents && this.OnConnectFailure != null)
					this.OnConnectFailure(this, "Already Connected to " + this.ServerName);
				return false;
			}

			if (this.EnableEvents && this.OnConnectStart != null)
				this.OnConnectStart(this, address);

			this.ServerName = address;

			// Create the underlying TCP Connection to the News Server
			try {
				this.tcpServer = new TcpClient(address, port);
				//this.tcpServer.ReceiveTimeout = this.Timeout;

				// Setup the Network Stream and its reader
				NetworkStream ns = this.tcpServer.GetStream();
				StreamReader r = new StreamReader(ns);

				// Read the welcome message
				string welcome = r.ReadLine();

				// Decode the welcome message from the Server
				switch (welcome.Substring(0, 3)) {
					case "200":
						this.IsConnected = true;
						this.IsPostingAllowed = true;
						break;
					case "201":
						this.IsConnected = true;
						this.IsPostingAllowed = false;
						break;
					case "400":
					case "502":
						this.IsPostingAllowed = this.IsConnected = false;
						break;
					default:
						throw new Exception("Unexpected Server Response while Connecting: " + welcome);
				}

				if (this.IsConnected) {
					if (this.EnableEvents && this.OnConnectSucceed != null)
						this.OnConnectSucceed(this, welcome);
					return true;
				} else {
					if (this.EnableEvents && this.OnConnectFailure != null)
						this.OnConnectFailure(this, welcome);
					return false;
				}
			}
			catch (Exception ex) {
				MessageBox.Show(
					"Error : " + ex.Message,
					"Connection Error!",
					MessageBoxButtons.OK,
					MessageBoxIcon.Exclamation,
					MessageBoxDefaultButton.Button1);
				this.IsConnected = this.IsPostingAllowed = false;
				return false;
			}
		}

		/// <summary>
		/// Disconnect from the current news server
		/// </summary>
		/// <exception cref="System.Exception">Thrown when network stream operation fails or 
		/// tcpclient close operation fails</exception>
		public void Disconnect()
		{
			if (!this.IsConnected) {
				return;
			}

			try {
				NetworkStream ns = this.tcpServer.GetStream();
				StreamReader sr = new StreamReader(ns);
				StreamWriter sw = new StreamWriter(ns);
				sw.AutoFlush = true;
				sw.WriteLine("QUIT");
				sr.ReadLine();
				this.IsConnected = false;
				this.IsPostingAllowed = false;
				this.ServerName = string.Empty;
				this.CurrentNewsgroup = null;
				sw.Close();
			}
			catch (Exception ex) {
				System.Windows.Forms.MessageBox.Show("Error : " + ex.Message,
					"Error During Signing Off Server!",
					System.Windows.Forms.MessageBoxButtons.OK,
					System.Windows.Forms.MessageBoxIcon.Exclamation,
					MessageBoxDefaultButton.Button1);
			}

			try {
				this.tcpServer.Close();
			}
			catch (Exception ex) {
				System.Windows.Forms.MessageBox.Show("Error : " + ex.Message,
					"Error Encountered During Disconnect!",
					System.Windows.Forms.MessageBoxButtons.OK,
					System.Windows.Forms.MessageBoxIcon.Exclamation,
					MessageBoxDefaultButton.Button1);
			}

			this.tcpServer = null;
			if (this.EnableEvents && this.OnDisconnect != null)
				this.OnDisconnect(this, "Disconnected from + " + this.ServerName);
		}

		/// <summary>
		/// List the Current Active newsgroup on the news server
		/// </summary>
		/// <returns>The number of newsgroup active</returns>
		/// <exception cref="System.Exception">Thrown when we have an invalid response the server</exception>
		public string[] ListGroup()
		{
			if (!this.IsConnected) {
				System.Windows.Forms.MessageBox.Show(
					"Not connected to any server!", "Error: Not Connected!",
					System.Windows.Forms.MessageBoxButtons.OK,
					System.Windows.Forms.MessageBoxIcon.Exclamation,
					MessageBoxDefaultButton.Button1);
				return null;
			}

			NetworkStream ns = this.tcpServer.GetStream();
			StreamReader sr = new StreamReader(ns);
			StreamWriter sw = new StreamWriter(ns);
			sw.AutoFlush = true;

			if (this.EnableEvents && this.OnListNewsgroupStart != null)
				this.OnListNewsgroupStart(this, this.ServerName);

			sw.WriteLine("LIST ACTIVE");
			string strResponse = sr.ReadLine();

			if (strResponse == null) {
				throw new Exception("LIST ACTIVE : NULL Response received from Server");
			}

			if (strResponse.StartsWith("215")) {
				bool done = false;
				string response;

				System.Collections.ArrayList newsgroups = new System.Collections.ArrayList();

				do {
					response = sr.ReadLine();
					if (response == null) {
						throw new Exception("LIST ACTIVE : NULL Response received from Server");
					}

					if (response == ".")
						done = true;
					else {
						string[] GroupNumbers = response.Split(' ');
						newsgroups.Add(GroupNumbers[0]);
						if (this.EnableEvents && this.OnListNewsgroupOne != null)
							this.OnListNewsgroupOne(this, GroupNumbers[0]);
					}
				}
				while (!done);

				if (this.EnableEvents && this.OnListNewsgroupDone != null)
					this.OnListNewsgroupDone(this, this.ServerName);

				return (string[])newsgroups.ToArray(Type.GetType("System.String", true));

			} else {
				throw new Exception("LIST ACTIVE : Unexpected response from server. Received : " + strResponse);
			}
		}

		/// <summary>
		/// Connects to a specific newsgroup on the news server
		/// </summary>
		/// <param name="group">Name of the newsgroup</param>
		/// <returns> Estimated number of post in the newsgroup.  -1 is returned if there's 
		/// no such group </returns>
		/// <exception cref="System.Exception">Thrown when an invalid response is received
		/// from the server</exception>
		public int SelectGroup(string group)
		{
			NetworkStream ns = this.tcpServer.GetStream();
			StreamReader sr = new StreamReader(ns);
			StreamWriter sw = new StreamWriter(ns);
			sw.AutoFlush = true;

			sw.WriteLine("GROUP " + group);
			string strResponse = sr.ReadLine();
			if (strResponse == null) {
				throw new Exception("GROUP : NULL Response received from server");
			}

			if (strResponse.StartsWith("211")) {
				string[] MessageNumbers;
				MessageNumbers = strResponse.Split(' ');
				int estMsgCount = Convert.ToInt32(MessageNumbers[1]);
				int low = Convert.ToInt32(MessageNumbers[2]);
				int high = Convert.ToInt32(MessageNumbers[3]);
				this.CurrentNewsgroup = new Newsgroup(group, estMsgCount, low, high);
				if (this.EnableEvents && this.OnSelectGroupSucceeded != null)
					this.OnSelectGroupSucceeded(this, String.Join(" ", MessageNumbers));
				return estMsgCount;
			} else if (strResponse.StartsWith("411")) {
				this.CurrentNewsgroup = null;
				if (this.EnableEvents && this.OnSelectGroupFailed != null)
					this.OnSelectGroupFailed(this, strResponse);
				return -1;
			} else {
				throw new Exception("GROUP : Unexpected Response from server;  Received: " + strResponse);
			}
		}

		/// <summary>
		/// Reconnect to the previously connected newsgroup
		/// </summary>
		/// <returns>Boolean variable indicating whether the connection is successful</returns>
		public bool ReconnectNewsgroup()
		{
			this.IsConnected = false;

			if (!this.Connect(this.ServerName, 119))
				return false;

			if (this.SelectGroup(this.CurrentNewsgroup.GroupName) != -1)
				return true;
			else
				return false;
		}

		/// <summary>
		/// Download all the headers from the selected newsgroup
		/// </summary>
		public int DownloadHeaders()
		{
			return this.DownloadHeaders(this.CurrentNewsgroup.Low, this.CurrentNewsgroup.High);
		}

		/// <summary>
		/// Download all headers between the range [fromMsg, toMsg]
		/// </summary>
		/// <param name="fromMsg">Lower bound of the MessageID</param>
		/// <param name="toMsg">Upper bound of the MessageID</param>
		public int DownloadHeaders(int fromMsg, int toMsg)
		{
			if (this.CurrentNewsgroup == null || !this.IsConnected) {
				if (this.OnDownloadHeaderFailed != null)
					this.OnDownloadHeaderFailed(this, new EventArgs());
				return -1;
			} else {
				string XOVERCommand = "XOVER " + fromMsg + "-" + toMsg;
				return this.DownloadHeaders(XOVERCommand);
			}
		}

		/// <summary>
		/// Execute the XOVER Command to retrieve the headers from the current newsgroup
		/// </summary>
		/// <param name="XOVERCommand">XOVER command string</param>
		/// <returns></returns>
		/// <exception cref="System.Exception">Thrown when an invalid response is received
		/// from the server</exception>
		private int DownloadHeaders(string XOVERCommand)
		{
			// Setup the network stream
			NetworkStream ns = this.tcpServer.GetStream();
			StreamReader sr = new StreamReader(ns, Encoding.Default);
			StreamWriter sw = new StreamWriter(ns);
			sw.AutoFlush = true;


			sw.WriteLine(XOVERCommand);				// Execute the XOVER command
			string response = sr.ReadLine();		// Retrieve Response

			int headerCount = 0;
			if (response == null)
				return headerCount;

			switch (response.Substring(0, 3)) {
				case "224":							// Valid command, article list to follow
					bool done = false;
					string article;
					do {
						article = sr.ReadLine();	// Read the Header of the article
						if (article == null)		// NULL response from server
							return headerCount;

						if (article == ".")		// Check for finish
							done = true;
						else {
							article.Replace("\t\t", "\t<empty>\t");		// take care of empty fields
							string[] fields = article.Split('\t');	// break up the server response

							ArticleHeader h = new ArticleHeader();		// fill in header
							h.ID = fields[0];
							h.Subject = Utilites.DecodeHeaderString(fields[1]);
							h.From = Utilites.DecodeHeaderString(fields[2]);
							h.Date = fields[3];
							headerCount++;

							if (this.EnableEvents && this.OnDownLoadHeaderOne != null)
								this.OnDownLoadHeaderOne(this, h);
						}
					} while (!done);

					if (this.OnDownloadHeaderSucceeded != null && this.EnableEvents)
						this.OnDownloadHeaderSucceeded(this, new EventArgs());
					headerCount = int.MaxValue;
					break;

				case "412":		// 412 No newsgroup selected
				case "420":		// 420 Current article number is invalid
				case "423":		// 423 No articles in that range
					if (this.OnDownloadHeaderFailed != null && this.EnableEvents)
						this.OnDownloadHeaderFailed(this, new EventArgs());
					headerCount = -1;
					break;
				default:
					throw new Exception("XOVER : Unexpected response from server. Received: " + response);
			}

			return headerCount;
		}

		public string DownloadArticleBody(string ArticleID)
		{
			return RetrieveMessage("BODY " + ArticleID, "222");
		}

		public string DownloadEntireArticle(string ArticleID)
		{
			return RetrieveMessage("ARTICLE " + ArticleID, "220");
		}

		public string DownloadArticleHeader(string ArticleID)
		{
			return RetrieveMessage("HEAD " + ArticleID, "221");
		}

		protected string RetrieveMessage(string RetrieveCommand, string ExpectedResponse)
		{
			if (CurrentNewsgroup == null) {
				throw new Exception("No group has been selected!");
			}

			NetworkStream ns = tcpServer.GetStream();
			StreamReader r = new StreamReader(ns, Encoding.Default);
			StreamWriter w = new StreamWriter(ns);
			w.AutoFlush = true;

			// send XOVER command
			w.WriteLine(RetrieveCommand);

			string Response;
			Response = r.ReadLine();

			if (Response.StartsWith(ExpectedResponse)) {
				// command valid, group list to follow
				bool Done = false;
				int LineCount = 0;
				StringBuilder MessageBody = new StringBuilder();
				string l;
				do {
					// read header
					l = r.ReadLine();
					// check if finished
					if (l == ".") {
						Done = true;
					} else {
						// remove padded '.'
						if (l.StartsWith("..")) {
							l = l.Remove(0, 1);
						}
						// line retrieved, append to body
						LineCount++;
						MessageBody.Append(l);
						MessageBody.Append("\r\n");

						// raise event	
						if (OnDownloadArticleLineEvent != null)
							OnDownloadArticleLineEvent(this, l);
					}
				} while (!Done);
				return MessageBody.ToString();
			}
			// something went wrong, throw an exception
			throw new Exception(Response);
		}

		#region IDisposable Members

		void IDisposable.Dispose()
		{
			Disconnect();
		}

		#endregion

		public void PostMessage(string subject, string body, string sender)
		{
			if (CurrentNewsgroup == null) {
				throw new Exception("No group has been selected!");
			}

			NetworkStream ns = tcpServer.GetStream();
			StreamReader r = new StreamReader(ns, Encoding.Default);
			StreamWriter w = new StreamWriter(ns);
			w.AutoFlush = true;

			w.WriteLine("POST");
			string response = r.ReadLine();

			if (response.Substring(0, 3) != "340") {
				throw new Exception(response);
			}

			StringBuilder str = new StringBuilder();
			str.Append("From: ");
			str.Append(sender);
			str.Append("\r\n");

			str.Append("Newsgroups: ");
			str.Append(CurrentNewsgroup.GroupName);
			str.Append("\r\n");

			str.Append("Subject: ");
			if (subject.ToLower().StartsWith("re: ")) {
				str.Append(subject);
			} else {
				str.Append("Re: ");
				str.Append(subject);
			}
			str.Append("\r\n\r\n");

			str.Append(body);
			str.Append("\r\n.\r\n");

			w.WriteLine(str.ToString());
			response = r.ReadLine();

			if (response.Substring(0, 3) != "240") {
				throw new Exception(response);
			}
		}
	}
}
